{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os, xml.etree.ElementTree as ET, numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "This folder contains a sample of the data that will be released as a part \r\n",
      "of the two 2014 i2b2 challenge tracks. \r\n",
      "\r\n",
      "Each track has its own folder of sample data; the contents are outlined \r\n",
      "below.\r\n",
      "\r\n",
      "----------------------------------------------\r\n",
      "\r\n",
      "Track 1: De-identification \r\n",
      "\r\n",
      "Folder contents:\r\n",
      "\r\n",
      "PHI/\r\n",
      "\tThis folder contains only PHI-related annotations for the text \r\n",
      "in each document.  These files are the gold standard for the de-id track.\r\n",
      "\r\n",
      "De-identification_guidelines_2014_distribution.pdf\r\n",
      "\tThis file contains the guidelines for PHI identification\r\n",
      "\r\n",
      "de-idi2b2_distribution.dtd\r\n",
      "\tThis file is the DTD that describes the valid tags for the \r\n",
      "de-identification annotation task.\r\n",
      "\r\n",
      "---------------------------------------------------\r\n",
      "\r\n",
      "Track 2: Identifying risk factors for heart disease over time\r\n",
      "\r\n",
      "Folder contents:\r\n",
      "\r\n",
      "gold/\r\n",
      "\tThis folder contains the gold standard annotations that will be used for\r\n",
      "evaluation. These are document-level tags which are defined in the annotation\r\n",
      "guidelines. Valid tags and attributes are outlined in the provided cardiac risk DTD\r\n",
      "\r\n",
      "complete/\r\n",
      "\tThis folder contains complete annotations for the entire text. It contains \r\n",
      "document level annotations. Each document level annotation is supplemented with tags \r\n",
      "that show the the evidence found by our annotators for a particular document level \r\n",
      "tag. These annotator tags include position information and are included with the hope \r\n",
      "that they will ease system development and error analysis.\r\n",
      "\r\n",
      "i2b2_2014_annotation_guidelines_distribution.pdf\r\n",
      "\tThis file contains the guidelines for the risk factor annotation \r\n",
      "\r\n",
      "cardiacRisk_distribution.dtd\r\n",
      "\tThis file is the DTD that describes the valid tags for the \r\n",
      "risk factor annotation task\r\n",
      "\r\n",
      "---------------------------------------------------\r\n",
      "\r\n",
      "Overview of the sample files:\r\n",
      "\r\n",
      "The gold/, complete/, and PHI/ folders each contain 8 XML files. These\r\n",
      "files contain four discharge summaries each for two different patients. The file\r\n",
      "names follow a consistent pattern with the first set of digits identifying the\r\n",
      "patient and the last set of digits identifying the sequential record number\r\n",
      "\r\n",
      "ie: XXX-YY.xml  \r\n",
      "where XXX is the patient number,  and YY is the record number.\r\n",
      "\r\n",
      "Example: 320-03.xml\r\n",
      "This is the third (03) record for patient 320\r\n",
      "\r\n",
      "Each file has a root level xml node <CardicaRiskTask> which will contain a\r\n",
      "<TEXT> node that holds the medical annotation text and a <TAGS> node containing\r\n",
      "annotations for the document text.  The specific annotations contained in each file \r\n",
      "are described by the accompanying DTDs and annotation guidelines.\r\n",
      "\r\n",
      "---------------------------------------------------\r\n",
      "\r\n"
     ]
    }
   ],
   "source": [
    "cat readme.txt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "START_CDATA = \"<TEXT><![CDATA[\"\n",
    "END_CDATA   = \"]]></TEXT>\"\n",
    "\n",
    "TAGS        = ['MEDICATION', 'OBSEE', 'SMOKER', 'HYPERTENSION', 'PHI', 'FAMILY_HIST']\n",
    "\n",
    "def read_xml_file(xml_path, PHI_tag_type='ALL_CHILDREN', match_text=True):\n",
    "    with open(xml_path, mode='r') as f:\n",
    "        lines = f.readlines()\n",
    "        text, in_text = [], False\n",
    "        for i, l in enumerate(lines):\n",
    "            if START_CDATA in l:\n",
    "                text.append(list(l[l.find(START_CDATA) + len(START_CDATA):]))\n",
    "                in_text = True\n",
    "            elif END_CDATA in l:\n",
    "                text.append(list(l[:l.find(END_CDATA)]))\n",
    "                break\n",
    "            elif in_text:\n",
    "                if xml_path.endswith('180-03.xml') and '0808' in l and 'Effingham' in l:\n",
    "                    print(\"Adjusting known error\")\n",
    "                    l = l[:9] + ' ' * 4 + l[9:]\n",
    "#                 elif xml_path.endswith('188-05.xml') and 'Johnson & Johnson' in l:\n",
    "#                     print(\"Adjusting known error\")\n",
    "#                     l = l.replace('&', 'and')\n",
    "                text.append(list(l))\n",
    "        \n",
    "    pos_transformer = {}\n",
    "    \n",
    "    linear_pos = 1\n",
    "    for line, sentence in enumerate(text):\n",
    "        for char_pos, char in enumerate(sentence):\n",
    "            pos_transformer[linear_pos] = (line, char_pos)\n",
    "            linear_pos += 1\n",
    "        \n",
    "    xml_parsed = ET.parse(xml_path)\n",
    "    tag_containers = xml_parsed.findall('TAGS')\n",
    "    assert len(tag_containers) == 1, \"Found multiple tag sets!\"\n",
    "    tag_container = tag_containers[0]\n",
    "    \n",
    "    PHI_tags = tag_container.getchildren() if PHI_tag_type == 'ALL_CHILDREN' else tag_container.findall('PHI')\n",
    "    PHI_labels = [['O'] * len(sentence) for sentence in text]\n",
    "    for PHI_tag in PHI_tags:\n",
    "        base_label = PHI_tag.attrib['TYPE']\n",
    "        start_pos, end_pos, PHI_text = PHI_tag.attrib['start'], PHI_tag.attrib['end'], PHI_tag.attrib['text']\n",
    "        start_pos, end_pos = int(start_pos)+1, int(end_pos)\n",
    "        PHI_text = ' '.join(PHI_text.split())\n",
    "#         if PHI_text == \"0808 O’neil’s Court\":\n",
    "#             print(\"Adjusting known error\")\n",
    "#             end_pos -= 4\n",
    "        if PHI_text == 'Johnson and Johnson' and xml_path.endswith('188-05.xml'):\n",
    "            print(\"Adjusting known error\")\n",
    "            PHI_text = 'Johnson & Johnson'\n",
    "        \n",
    "\n",
    "        (start_line, start_char), (end_line, end_char) = pos_transformer[start_pos], pos_transformer[end_pos]\n",
    "            \n",
    "        obs_text = []\n",
    "        for line in range(start_line, end_line+1):\n",
    "            t = text[line]\n",
    "            s = start_char if line == start_line else 0\n",
    "            e = end_char if line == end_line else len(t)\n",
    "            obs_text.append(''.join(t[s:e+1]).strip())\n",
    "        obs_text = ' '.join(obs_text)\n",
    "        obs_text = ' '.join(obs_text.split())\n",
    "              \n",
    "        if match_text: assert obs_text == PHI_text, (\n",
    "            (\"Texts don't match! %s v %s\" % (PHI_text, obs_text)) + '\\n' + str((\n",
    "                start_pos, end_pos, line, s, e, t, xml_path\n",
    "            ))\n",
    "        )\n",
    "        \n",
    "        PHI_labels[end_line][end_char]     = 'I-%s' % base_label\n",
    "        PHI_labels[start_line][start_char] = 'B-%s' % base_label\n",
    "        \n",
    "        for line in range(start_line, end_line+1):\n",
    "            t = text[line]\n",
    "            s = start_char+1 if line == start_line else 0\n",
    "            e = end_char-1 if line == end_line else len(t)-1\n",
    "            for i in range(s, e+1): PHI_labels[line][i] = 'I-%s' % base_label\n",
    "\n",
    "    return text, PHI_labels\n",
    "    \n",
    "def merge_into_words(text_by_char, all_labels_by_char):\n",
    "    assert len(text_by_char) == len(all_labels_by_char), \"Incorrect # of sentences!\"\n",
    "    \n",
    "    N = len(text_by_char)\n",
    "    \n",
    "    text_by_word, all_labels_by_word = [], []\n",
    "    \n",
    "    for sentence_num in range(N):\n",
    "        sentence_by_char = text_by_char[sentence_num]\n",
    "        labels_by_char   = all_labels_by_char[sentence_num]\n",
    "        \n",
    "        assert len(sentence_by_char) == len(labels_by_char), \"Incorrect # of chars in sentence!\"\n",
    "        S = len(sentence_by_char)\n",
    "        \n",
    "        if labels_by_char == (['O'] * len(sentence_by_char)):\n",
    "            sentence_by_word = ''.join(sentence_by_char).split()\n",
    "            labels_by_word   = ['O'] * len(sentence_by_word)\n",
    "        else: \n",
    "            sentence_by_word, labels_by_word = [], []\n",
    "            text_chunks, labels_chunks = [], []\n",
    "            s = 0\n",
    "            for i in range(S):\n",
    "                if i == S-1:\n",
    "                    text_chunks.append(sentence_by_char[s:])\n",
    "                    labels_chunks.append(labels_by_char[s:])\n",
    "                elif labels_by_char[i] == 'O': continue\n",
    "                else:\n",
    "                    if i > 0 and labels_by_char[i-1] == 'O':\n",
    "                        text_chunks.append(sentence_by_char[s:i])\n",
    "                        labels_chunks.append(labels_by_char[s:i])\n",
    "                        s = i\n",
    "                    if labels_by_char[i+1] == 'O' or labels_by_char[i+1][2:] != labels_by_char[i][2:]:\n",
    "                        text_chunks.append(sentence_by_char[s:i+1])\n",
    "                        labels_chunks.append(labels_by_char[s:i+1])\n",
    "                        s = i+1\n",
    "                \n",
    "            for text_chunk, labels_chunk in zip(text_chunks, labels_chunks):\n",
    "                assert len(text_chunk) == len(labels_chunk), \"Bad Chunking (len)\"\n",
    "                assert len(text_chunk) > 0, \"Bad chunking (len 0)\" + str(text_chunks) + str(labels_chunks)\n",
    "                \n",
    "                labels_set = set(labels_chunk)\n",
    "                assert labels_set == set(['O']) or (len(labels_set) <= 3 and 'O' not in labels_set), (\n",
    "                    (\"Bad chunking (contents) %s\" % ', '.join(labels_set))+ str(text_chunks) + str(labels_chunks)\n",
    "                )\n",
    "                \n",
    "                text_chunk_by_word = ''.join(text_chunk).split()\n",
    "                W = len(text_chunk_by_word)\n",
    "                if W == 0: \n",
    "#                     assert labels_set == set(['O']), \"0-word chunking and non-0 label!\" + str(\n",
    "#                         text_chunks) + str(labels_chunks\n",
    "#                     )\n",
    "                    continue\n",
    "                \n",
    "                if labels_chunk[0] == 'O': labels_chunk_by_word = ['O'] * W\n",
    "                elif W == 1:               labels_chunk_by_word = [labels_chunk[0]]\n",
    "                elif W == 2:               labels_chunk_by_word = [labels_chunk[0], labels_chunk[-1]]\n",
    "                else:                      labels_chunk_by_word = [\n",
    "                        labels_chunk[0]\n",
    "                    ] + [labels_chunk[1]] * (W - 2) + [\n",
    "                        labels_chunk[-1]\n",
    "                    ]\n",
    "                    \n",
    "                sentence_by_word.extend(text_chunk_by_word)\n",
    "                labels_by_word.extend(labels_chunk_by_word)\n",
    "\n",
    "        assert len(sentence_by_word) == len(labels_by_word), \"Incorrect # of words in sentence!\"    \n",
    "        \n",
    "        if len(sentence_by_word) == 0: continue\n",
    "            \n",
    "        text_by_word.append(sentence_by_word)\n",
    "        all_labels_by_word.append(labels_by_word)\n",
    "    return text_by_word, all_labels_by_word\n",
    "\n",
    "def reprocess_PHI_labels(folders, base_path='.', PHI_tag_type='PHI', match_text=True, dev_set_size=None):\n",
    "    all_texts_by_patient, all_labels_by_patient = {}, {}\n",
    "\n",
    "    for folder in folders:\n",
    "        folder_dir = os.path.join(base_path, folder)\n",
    "        xml_filenames = [x for x in os.listdir(folder_dir) if x.endswith('xml')]\n",
    "        for xml_filename in xml_filenames:\n",
    "            patient_num = int(xml_filename[:3])\n",
    "            xml_filepath = os.path.join(folder_dir, xml_filename)\n",
    "            \n",
    "            text_by_char, labels_by_char = read_xml_file(\n",
    "                xml_filepath,\n",
    "                PHI_tag_type=PHI_tag_type,\n",
    "                match_text=match_text\n",
    "            )\n",
    "            text_by_word, labels_by_word = merge_into_words(text_by_char, labels_by_char)\n",
    "            \n",
    "            if patient_num not in all_texts_by_patient:\n",
    "                all_texts_by_patient[patient_num] = []\n",
    "                all_labels_by_patient[patient_num] = []\n",
    "            \n",
    "            all_texts_by_patient[patient_num].extend(text_by_word)\n",
    "            all_labels_by_patient[patient_num].extend(labels_by_word)\n",
    "            \n",
    "    patients = set(all_texts_by_patient.keys())\n",
    "    \n",
    "    if dev_set_size is None: train_patients, dev_patients = list(patients), []\n",
    "    else:\n",
    "        N_train = int(len(patients) * (1-dev_set_size))\n",
    "        patients_random = np.random.permutation(list(patients))\n",
    "        train_patients = list(patients_random[:N_train])\n",
    "        dev_patients   = list(patients_random[N_train:])\n",
    "    \n",
    "    train_texts, train_labels = [], []\n",
    "    dev_texts, dev_labels = [], []\n",
    "    \n",
    "    for patient_num in train_patients:\n",
    "        train_texts.extend(all_texts_by_patient[patient_num])\n",
    "        train_labels.extend(all_labels_by_patient[patient_num])\n",
    "\n",
    "    for patient_num in dev_patients:\n",
    "        dev_texts.extend(all_texts_by_patient[patient_num])\n",
    "        dev_labels.extend(all_labels_by_patient[patient_num])\n",
    "\n",
    "\n",
    "    train_out_text_by_sentence = []\n",
    "    for text, labels in zip(train_texts, train_labels):\n",
    "        train_out_text_by_sentence.append('\\n'.join('%s %s' % x for x in zip(text, labels)))\n",
    "    dev_out_text_by_sentence = []\n",
    "    for text, labels in zip(dev_texts, dev_labels):\n",
    "        dev_out_text_by_sentence.append('\\n'.join('%s %s' % x for x in zip(text, labels)))\n",
    "\n",
    "    return '\\n\\n'.join(train_out_text_by_sentence), '\\n\\n'.join(dev_out_text_by_sentence)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adjusting known error\n",
      "Adjusting known error\n"
     ]
    }
   ],
   "source": [
    "final_train_text, final_dev_text = reprocess_PHI_labels(\n",
    "    ['training-PHI-Gold-Set1/', 'training-PHI-Gold-Set2/'], PHI_tag_type='ALL_CHILDREN',\n",
    "    dev_set_size=0.1, match_text=True\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_text, _ = reprocess_PHI_labels(\n",
    "    ['testing-PHI-Gold-fixed'], PHI_tag_type='ALL_CHILDREN', match_text=False, dev_set_size=None\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Record O\n",
      "date: O\n",
      "2067-11-30 B-DATE\n",
      "\n",
      "CARDIOLOGY O\n",
      "\n",
      "OCHILTREE B-HOSPITAL\n",
      "GENERAL I-HOSPITAL\n",
      "HOSPITAL I-HOSPITAL\n",
      "\n",
      "Reason O\n",
      "for O\n",
      "visit: O\n",
      "\n",
      "Follow-up O\n",
      "appointment O\n",
      "for O\n",
      "cardiomyopathy O\n",
      "\n",
      "Interval O\n",
      "History: O\n",
      "\n",
      "Mr O\n",
      "Lara B-PATIENT\n",
      "reports O\n",
      "no O\n",
      "problems O\n",
      "since O\n",
      "the O\n",
      "time O\n",
      "of O\n",
      "his O\n",
      "last O\n",
      "visit. O\n",
      "He O\n",
      "insistists O\n",
      "that O\n",
      "he O\n",
      "has O\n",
      "absolutely O\n",
      "abstained O\n",
      "from O\n",
      "etoh O\n",
      "since O\n",
      "his O\n",
      "last O\n",
      "visit. O\n",
      "\n",
      "Denies O\n",
      "fevers, O\n",
      "chills. O\n",
      "denies O\n",
      "palpitations O\n",
      "or O\n",
      "syncope. O\n",
      "\n",
      "Under O\n",
      "a O\n",
      "fair O\n",
      "am\n"
     ]
    }
   ],
   "source": [
    "print(final_train_text[:500])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Record O\n",
      "date: O\n",
      "2082-02-28 B-DATE\n",
      "\n",
      "c.c. O\n",
      "Preop O\n",
      "PE O\n",
      "for O\n",
      "cataract O\n",
      "extraction O\n",
      "on O\n",
      "3/7 B-DATE\n",
      "\n",
      "S: O\n",
      "ROS: O\n",
      "General: O\n",
      "no O\n",
      "history O\n",
      "of O\n",
      "fatigue, O\n",
      "weight O\n",
      "change, O\n",
      "loss O\n",
      "of O\n",
      "appetite, O\n",
      "or O\n",
      "weakness. O\n",
      "\n",
      "HEENT: O\n",
      "no O\n",
      "history O\n",
      "of O\n",
      "head O\n",
      "injury, O\n",
      "glaucoma, O\n",
      "tinnitus, O\n",
      "vertigo, O\n",
      "motion O\n",
      "sickness, O\n",
      "URI, O\n",
      "hearing O\n",
      "loss. O\n",
      "\n",
      "Cardiovascular: O\n",
      "+ O\n",
      "hypertension O\n",
      "- O\n",
      "adequate O\n",
      "co\n"
     ]
    }
   ],
   "source": [
    "print(final_dev_text[:400])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Record O\n",
      "date: O\n",
      "2080-02-18 B-DATE\n",
      "\n",
      "SDU O\n",
      "JAR O\n",
      "Admission O\n",
      "Note O\n",
      "\n",
      "Name: O\n",
      "Yosef B-PATIENT\n",
      "Villegas I-PATIENT\n",
      "\n",
      "MR: O\n",
      "8249813 B-MEDICALRECORD\n",
      "\n",
      "DOA: O\n",
      "2/17/80 B-DATE\n",
      "\n",
      "PCP: O\n",
      "Gilbert B-DOCTOR\n",
      "Perez I-DOCTOR\n",
      "\n",
      "Attending: O\n",
      "YBARRA B-DOCTOR\n",
      "\n",
      "CODE: O\n",
      "FULL O\n",
      "\n",
      "HPI: O\n",
      "70 B-AGE\n",
      "yo O\n",
      "M O\n",
      "with O\n",
      "NIDDM O\n",
      "admitted O\n",
      "for O\n",
      "cath O\n",
      "after O\n",
      "positive O\n",
      "MIBI. O\n",
      "Pt O\n",
      "has O\n",
      "had O\n",
      "increasing O\n",
      "CP O\n",
      "and O\n",
      "SOB O\n",
      "on O\n",
      "exert\n"
     ]
    }
   ],
   "source": [
    "print(test_text[:400])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "labels = {}\n",
    "for s in final_train_text, final_dev_text, test_text:\n",
    "    for line in s.split('\\n'):\n",
    "        if line == '': continue\n",
    "        label = line.split()[-1]\n",
    "        assert label == 'O' or label.startswith('B-') or label.startswith('I-'), \"label wrong! %s\" % label\n",
    "        if label not in labels: labels[label] = 1\n",
    "        else: labels[label] += 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'O': 777860,\n",
       " 'B-DATE': 12459,\n",
       " 'B-HOSPITAL': 2312,\n",
       " 'I-HOSPITAL': 1835,\n",
       " 'B-PATIENT': 2195,\n",
       " 'B-AGE': 1997,\n",
       " 'B-DOCTOR': 4797,\n",
       " 'I-DOCTOR': 3482,\n",
       " 'I-DATE': 1379,\n",
       " 'B-COUNTRY': 183,\n",
       " 'B-PROFESSION': 413,\n",
       " 'I-PROFESSION': 346,\n",
       " 'B-ORGANIZATION': 206,\n",
       " 'I-ORGANIZATION': 173,\n",
       " 'B-STREET': 352,\n",
       " 'I-STREET': 717,\n",
       " 'B-CITY': 654,\n",
       " 'I-CITY': 171,\n",
       " 'B-STATE': 504,\n",
       " 'B-ZIP': 352,\n",
       " 'I-PATIENT': 1192,\n",
       " 'B-MEDICALRECORD': 1033,\n",
       " 'I-MEDICALRECORD': 47,\n",
       " 'B-IDNUM': 456,\n",
       " 'B-USERNAME': 356,\n",
       " 'B-PHONE': 524,\n",
       " 'I-COUNTRY': 21,\n",
       " 'I-PHONE': 100,\n",
       " 'I-AGE': 10,\n",
       " 'I-IDNUM': 30,\n",
       " 'B-FAX': 10,\n",
       " 'I-FAX': 2,\n",
       " 'B-DEVICE': 15,\n",
       " 'B-EMAIL': 5,\n",
       " 'B-LOCATION-OTHER': 17,\n",
       " 'I-LOCATION-OTHER': 15,\n",
       " 'I-STATE': 18,\n",
       " 'B-URL': 2,\n",
       " 'I-URL': 4,\n",
       " 'B-BIOID': 1,\n",
       " 'B-HEALTHPLAN': 1,\n",
       " 'I-HEALTHPLAN': 1,\n",
       " 'I-DEVICE': 2}"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "labels"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [],
   "source": [
    "with open('./processed/train.tsv', mode='w') as f:\n",
    "    f.write(final_train_text)\n",
    "with open('./processed/dev.tsv', mode='w') as f:\n",
    "    f.write(final_dev_text)\n",
    "with open('./processed/test.tsv', mode='w') as f:\n",
    "    f.write(test_text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
